package imageintegrations

import (
	"context"
	"errors"
	"testing"
	"time"

	iiMocks "github.com/stackrox/rox/central/imageintegration/datastore/mocks"
	"github.com/stackrox/rox/central/sensor/service/pipeline/reconciliation"
	"github.com/stackrox/rox/generated/internalapi/central"
	"github.com/stackrox/rox/generated/storage"
	"github.com/stackrox/rox/pkg/env"
	"github.com/stackrox/rox/pkg/features"
	"github.com/stackrox/rox/pkg/openshift"
	"github.com/stackrox/rox/pkg/protocompat"
	"github.com/stackrox/rox/pkg/testutils"
	"github.com/stackrox/rox/pkg/tlscheckcache"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
)

func TestParseEndpoint(t *testing.T) {
	cases := []struct {
		endpoint string

		url string
	}{
		{
			endpoint: "https://docker.io",
			url:      "https://registry-1.docker.io",
		},
		{
			endpoint: "docker.io",
			url:      "https://registry-1.docker.io",
		},
		{
			endpoint: "index.docker.io",
			url:      "https://registry-1.docker.io",
		},
		{
			endpoint: "registry-1.docker.io",
			url:      "https://registry-1.docker.io",
		},
		{
			endpoint: "https://registry-1.docker.io",
			url:      "https://registry-1.docker.io",
		},
		{
			endpoint: "https://index.docker.io",
			url:      "https://registry-1.docker.io",
		},
		{
			endpoint: "https://index.docker.io/v1",
			url:      "https://registry-1.docker.io",
		},
		{
			endpoint: "https://myregistry.hello.io",
			url:      "https://myregistry.hello.io",
		},
		{
			endpoint: "myregistry.hello.io",
			url:      "https://myregistry.hello.io",
		},
		{
			endpoint: "myregistry.hello.io/v1/randompage",
			url:      "https://myregistry.hello.io",
		},
		{
			endpoint: "http://myregistry.hello.io/v1/randompage",
			url:      "http://myregistry.hello.io",
		},
		{
			endpoint: "http://myregistry.hello.io:5000/v1/randompage",
			url:      "http://myregistry.hello.io:5000",
		},
	}
	for _, c := range cases {
		t.Run(c.endpoint, func(t *testing.T) {
			url := parseEndpointForURL(c.endpoint)
			assert.Equal(t, c.url, url)
		})
	}
}

func Test_matchesECRAuth(t *testing.T) {
	createIntegration := func(authData *storage.ECRConfig_AuthorizationData) *storage.ImageIntegration {
		return &storage.ImageIntegration{
			IntegrationConfig: &storage.ImageIntegration_Ecr{
				Ecr: &storage.ECRConfig{
					AuthorizationData: authData,
				},
			},
		}
	}
	createAuthData := func(u, p string, e time.Time) *storage.ECRConfig_AuthorizationData {
		ts, err := protocompat.ConvertTimeToTimestampOrError(e)
		if err != nil {
			assert.FailNow(t, "failed to convert timestamp: %v", err)
		}
		return &storage.ECRConfig_AuthorizationData{
			Username:  u,
			Password:  p,
			ExpiresAt: ts,
		}
	}
	type args struct {
		this  *storage.ImageIntegration
		other *storage.ImageIntegration
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "should match on nil",
			args: args{
				this:  createIntegration(nil),
				other: createIntegration(nil),
			},
			want: true,
		},
		{
			name: "should match on same username, password and expire at",
			args: args{
				this:  createIntegration(createAuthData("foo", "bar", time.Unix(0, 0))),
				other: createIntegration(createAuthData("foo", "bar", time.Unix(0, 0))),
			},
			want: true,
		},
		{
			name: "should not match on different username",
			args: args{
				this:  createIntegration(createAuthData("foo", "bar", time.Unix(0, 0))),
				other: createIntegration(createAuthData("otherfoo", "bar", time.Unix(0, 0))),
			},
		},
		{
			name: "should not match on different password",
			args: args{
				this:  createIntegration(createAuthData("foo", "bar", time.Unix(0, 0))),
				other: createIntegration(createAuthData("foo", "password", time.Unix(0, 0))),
			},
		},
		{
			name: "should not match on different expired",
			args: args{
				this:  createIntegration(createAuthData("foo", "bar", time.Unix(0, 0))),
				other: createIntegration(createAuthData("foo", "password", time.Unix(10, 10))),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equalf(t, tt.want, matchesECRAuth(tt.args.this, tt.args.other), "matchesECRAuth(%v, %v)", tt.args.this, tt.args.other)
		})
	}
}

func TestSourcedImageIntegration(t *testing.T) {
	iiNoSrc := &storage.ImageIntegration{}
	iiGlobalPull := &storage.ImageIntegration{
		Source: &storage.ImageIntegration_Source{
			Namespace:           openshift.GlobalPullSecretNamespace,
			ImagePullSecretName: openshift.GlobalPullSecretName,
		},
	}
	iiSourcedOther := &storage.ImageIntegration{
		Source: &storage.ImageIntegration_Source{
			Namespace:           "fake-namespace",
			ImagePullSecretName: "fake-secretname",
		},
	}

	t.Run("no integrations sourced when features disabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, false)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), "false")

		assert.False(t, sourcedImageIntegration(iiNoSrc))
		assert.False(t, sourcedImageIntegration(iiGlobalPull))
		assert.False(t, sourcedImageIntegration(iiSourcedOther))
	})

	t.Run("all integrations with source are sourced when main feature enabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, true)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), "false")

		assert.False(t, sourcedImageIntegration(iiNoSrc))
		assert.True(t, sourcedImageIntegration(iiGlobalPull))
		assert.True(t, sourcedImageIntegration(iiSourcedOther))
	})

	t.Run("all integrations with source are sourced when all features enabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, true)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), "true")

		assert.False(t, sourcedImageIntegration(iiNoSrc))
		assert.True(t, sourcedImageIntegration(iiGlobalPull))
		assert.True(t, sourcedImageIntegration(iiSourcedOther))
	})

	t.Run("only global pull integration sourced when global pull feature enabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, false)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), "true")

		assert.False(t, sourcedImageIntegration(iiNoSrc))
		assert.True(t, sourcedImageIntegration(iiGlobalPull))
		assert.False(t, sourcedImageIntegration(iiSourcedOther))
	})
}

func TestSetupIntegrationParamsDescription(t *testing.T) {
	ctx := context.Background()
	ii := &storage.ImageIntegration{
		IntegrationConfig: &storage.ImageIntegration_Docker{
			Docker: &storage.DockerConfig{
				Endpoint: "registry.invalid/path/to/thing",
			},
		},
	}

	t.Run("do not include path in description", func(t *testing.T) {
		desc, _, _ := setUpIntegrationParams(ctx, ii.CloneVT(), false)
		assert.NotContains(t, desc, "/path")
		assert.Equal(t, "https://registry.invalid", desc)
	})

	t.Run("include path in description", func(t *testing.T) {
		desc, _, _ := setUpIntegrationParams(ctx, ii.CloneVT(), true)
		assert.Equal(t, "https://registry.invalid/path/to/thing", desc)
	})
}

func TestReconcileGlobalPullSecret(t *testing.T) {
	bCtx := context.Background()
	clusterID := "cluster-id"

	t.Run("no reconcile when features disabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, false)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), "false")

		p := pipelineImpl{}
		assert.NoError(t, p.Reconcile(bCtx, clusterID, nil))
	})

	t.Run("reconcile when feature enabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, false)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), "true")

		ctrl := gomock.NewController(t)
		iiDSMock := iiMocks.NewMockDataStore(ctrl)

		p := pipelineImpl{datastore: iiDSMock}
		m := reconciliation.NewStoreMap()
		m.Add((*central.SensorEvent_ImageIntegration)(nil), "global-pull-1")

		globalPullSource := &storage.ImageIntegration_Source{
			ImagePullSecretName: openshift.GlobalPullSecretName,
			Namespace:           openshift.GlobalPullSecretNamespace,
		}

		integrations := []*storage.ImageIntegration{
			{Id: "not-global-pull", ClusterId: clusterID},
			{Id: "global-pull-1", ClusterId: clusterID, Source: globalPullSource},
			{Id: "global-pull-2", ClusterId: clusterID, Source: globalPullSource},
		}

		iiDSMock.EXPECT().GetImageIntegrations(bCtx, gomock.Any()).Return(integrations, nil)

		// Only the integration not in the map populated from sensor 'sync' events
		// should be removed (reconciled).
		iiDSMock.EXPECT().RemoveImageIntegration(bCtx, "global-pull-2")

		assert.NoError(t, p.Reconcile(bCtx, clusterID, m))
	})
}

func TestUpdateIntegrationInsecuredFlag(t *testing.T) {
	ctx := context.Background()
	endpoint := "endpoint.invalid"

	tlsCheckErrFunc := func(_ context.Context, _ string) (bool, error) {
		return false, errors.New("fake tls failure")
	}

	tlsCheckInsecureFunc := func(_ context.Context, _ string) (bool, error) {
		return false, nil
	}

	tlsCheckSecureFunc := func(_ context.Context, _ string) (bool, error) {
		return true, nil
	}

	createII := func(insecure bool) *storage.ImageIntegration {
		return &storage.ImageIntegration{
			IntegrationConfig: &storage.ImageIntegration_Docker{
				Docker: &storage.DockerConfig{
					Endpoint: endpoint,
					Insecure: insecure,
				},
			},
		}
	}

	t.Run("insecure is false on tls check error", func(t *testing.T) {
		p := pipelineImpl{
			tlsCheckCache: tlscheckcache.New(
				tlscheckcache.WithTLSCheckFunc(tlsCheckErrFunc),
			),
		}

		ii := createII(true)
		require.True(t, ii.GetDocker().GetInsecure())

		p.updateIntegrationInsecureFlag(ctx, ii)
		assert.False(t, ii.GetDocker().GetInsecure())
	})

	t.Run("insecure is false on tls check skip", func(t *testing.T) {
		cache := tlscheckcache.New(
			tlscheckcache.WithTLSCheckFunc(tlsCheckErrFunc),
		)
		p := pipelineImpl{
			tlsCheckCache: cache,
		}

		// Warm the cache with an error so next invocation will trigger a skip.
		_, skip, err := cache.CheckTLS(ctx, endpoint)
		require.False(t, skip)
		require.Error(t, err)

		// Confirm cache is returning skips
		_, skip, err = cache.CheckTLS(ctx, endpoint)
		require.True(t, skip)
		require.NoError(t, err)

		ii := createII(true)
		require.True(t, ii.GetDocker().GetInsecure())

		p.updateIntegrationInsecureFlag(ctx, ii)
		assert.False(t, ii.GetDocker().GetInsecure())
	})

	t.Run("insecure is false on tls check secure result", func(t *testing.T) {
		p := pipelineImpl{
			tlsCheckCache: tlscheckcache.New(
				tlscheckcache.WithTLSCheckFunc(tlsCheckSecureFunc),
			),
		}

		ii := createII(true)
		require.True(t, ii.GetDocker().GetInsecure())

		p.updateIntegrationInsecureFlag(ctx, ii)
		assert.False(t, ii.GetDocker().GetInsecure())
	})

	t.Run("insecure is true on tls check insecure result", func(t *testing.T) {
		p := pipelineImpl{
			tlsCheckCache: tlscheckcache.New(
				tlscheckcache.WithTLSCheckFunc(tlsCheckInsecureFunc),
			),
		}

		ii := createII(false)
		require.False(t, ii.GetDocker().GetInsecure())

		p.updateIntegrationInsecureFlag(ctx, ii)
		assert.True(t, ii.GetDocker().GetInsecure())
	})
}
